﻿using System;
using System.Collections.Generic;
using GeoAPI.Geometries;
using GisSharpBlog.NetTopologySuite.Geometries;

namespace AzukiMap.Tools
{
    public class DatelineConnection
    {
        private readonly GeometryUtil _geomUtil;
        private double _eastEnd = 180;
        private double _northEnd = 90;
        private double _southEnd = -90;
        private double _westEnd = -180;

        public DatelineConnection()
        {
            _geomUtil = new GeometryUtil();
        }


        public bool Is180Connected { get; set; }

        /// <summary>
        /// Max X for connecting the east + west canvases
        /// </summary>
        public double EastEnd
        {
            get { return _eastEnd; }
            set { _eastEnd = value; }
        }

        /// <summary>
        /// Min X for connection the east + west canvases.
        /// </summary>
        public double WestEnd
        {
            get { return _westEnd; }
            set { _westEnd = value; }
        }

        public double NorthEnd
        {
            get { return _northEnd; }
            set { _northEnd = value; }
        }

        public double SouthEnd
        {
            get { return _southEnd; }
            set { _southEnd = value; }
        }

        public double WorldWidth
        {
            get { return _eastEnd - _westEnd; }
        }

        public double HalfWorldWidth
        {
            get { return WorldWidth/2; }
        }

        private ILineString Dateline
        {
            get
            {
                var datelineNorth = new Coordinate(_eastEnd, _northEnd);
                var datelineSouth = new Coordinate(_eastEnd, _southEnd);
                return _geomUtil.CreateLineString(new ICoordinate[] {datelineNorth, datelineSouth});
            }
        }

        private IPolygon RealWorld
        {
            get
            {
                
                return _geomUtil.CreateExtentPolygon(_westEnd, _southEnd, _eastEnd, _northEnd);
            }
        }

        private IPolygon NextWorld
        {
            get { return _geomUtil.CreateExtentPolygon(_westEnd + WorldWidth, _southEnd, _eastEnd + WorldWidth, _northEnd); }
        }

        /// <summary>
        /// Returns regulated extent. 
        /// </summary>
        /// <param name="minX"></param>
        /// <param name="minY"></param>
        /// <param name="maxX"></param>
        /// <param name="maxY"></param>
        /// <returns></returns>
        public IMultiPolygon RegulateExtent(double minX, double minY, double maxX, double maxY)
        {
            if (double.IsNaN(minY) || double.IsNaN(minX) || double.IsNaN(maxY) || double.IsNaN(maxX))
            {
                throw new ArgumentException();
            }

            // Contentsをnew
            var minMaxes = new List<MinMax>();

            // この関数で使用するため引数の値をコピー
            double normalMinX = minX;
            double normalMaxX = maxX;

            // 緯度数値正規化（lonBeginのほうがlonEndより大きい）
            if (normalMaxX < normalMinX)
            {
                double swap = normalMaxX;
                normalMaxX = normalMinX;
                normalMinX = swap;
            }

            // 全面パターンを個別処理
            if (normalMaxX - normalMinX >= WorldWidth)
            {
                var latMinMaxContent = new MinMax
                                           {
                                               MinX = EastEnd,
                                               MinY = minY,
                                               MaxX = WestEnd,
                                               MaxY = maxY
                                           };
                minMaxes.Add(latMinMaxContent);
                return MinMaxesToMultiPolygon(minMaxes);
            }

            // 経度数値正規化（-180 ～ 180、latEndLocateNormalがlatBeginLocateNormalより大きい）
            while (EastEnd < normalMinX)
            {
                normalMinX -= WorldWidth;
                normalMaxX -= WorldWidth;
            }

            while (normalMinX < WestEnd)
            {
                normalMinX += WorldWidth;
                normalMaxX += WorldWidth;
            }

            while (normalMinX > normalMaxX)
            {
                normalMaxX += WorldWidth;
            }

            // ２つあるパターン
            if (normalMaxX >= 180)
            {
                //分割方式は見栄えがしないのでやめ
                /* 
                var latMinMaxContent1 = new MinMax
                                            {
                                                MinX = -HalfWorldWidth,
                                                MinY = minY,
                                                MaxX = normalMaxX - WorldWidth,
                                                MaxY = maxY
                                            };
                minMaxes.Add(latMinMaxContent1);
                var latMinMaxContent2 = new MinMax
                                            {
                                                MinX = normalMinX,
                                                MinY = minY,
                                                MaxX = HalfWorldWidth,
                                                MaxY = maxY
                                            };
                minMaxes.Add(latMinMaxContent2);
                 */
                // 分身方式
                var latMinMaxContent1 = new MinMax
                                            {
                                                MinX = normalMinX,
                                                MinY = minY,
                                                MaxX = normalMaxX,
                                                MaxY = maxY
                                            };
                minMaxes.Add(latMinMaxContent1);
                var latMinMaxContent2 = new MinMax
                                            {
                                                MinX = normalMinX - WorldWidth,
                                                MinY = minY,
                                                MaxX = normalMaxX - WorldWidth,
                                                MaxY = maxY
                                            };
                minMaxes.Add(latMinMaxContent2);
            }
            // ひとつのパターン
            else
            {
                var latMinMaxContent = new MinMax
                                           {
                                               MinX = normalMinX,
                                               MinY = minY,
                                               MaxX = normalMaxX,
                                               MaxY = maxY
                                           };
                minMaxes.Add(latMinMaxContent);
            }

            return MinMaxesToMultiPolygon(minMaxes);
        }

        /// <summary>
        /// IMultiPolygonで返す
        /// </summary>
        private static IMultiPolygon MinMaxesToMultiPolygon(List<MinMax> minMaxes)
        {
            var iPolygon = new List<IPolygon>(minMaxes.Count);
            foreach (MinMax minMax in minMaxes)
            {
                var coords = new List<Coordinate>
                                 {
                                     // 北西（左上）を基準に時計回り
                                     new Coordinate(minMax.MinX, minMax.MaxY),
                                     new Coordinate(minMax.MaxX, minMax.MaxY),
                                     new Coordinate(minMax.MaxX, minMax.MinY),
                                     new Coordinate(minMax.MinX, minMax.MinY),
                                     new Coordinate(minMax.MinX, minMax.MaxY)
                                 };

                var linearRing = new LinearRing(coords.ToArray());
                iPolygon.Add(new Polygon(linearRing));
            }

            var multiPolygon = new MultiPolygon(iPolygon.ToArray());
            return multiPolygon;
        }

        /// <summary>
        /// 世界の外にある経度を世界に戻す
        /// </summary>
        /// <param name="lonOrg"></param>
        /// <returns></returns>
        public double GetRegulatedLon(double lonOrg)
        {
            if (! Is180Connected)
            {
                return lonOrg;
            }
            double lon = lonOrg;

            while (_eastEnd < lon)
            {
                lon -= WorldWidth;
            }

            while (lon < _westEnd)
            {
                lon += WorldWidth;
            }
            return lon;
        }

        public IMultiPolygon GetRegulatedPolygon(IPolygon polygon)
        {
            if (! Is180Connected)
            {
                return new MultiPolygon(new[] {polygon});
            }

            // move a polygon origin to the real world.
            IPolygon movedPoly = MovePolyToTheWorld(polygon);

            if (! movedPoly.Intersects(Dateline))
            {
                return GetMultiPolygon(movedPoly);
            }

            // move a polygon origin to the minus world.
            IPolygon minusPoly = MovePolyToAnotherWorld(movedPoly);

            return new MultiPolygon(new[] {movedPoly, minusPoly});
        }

        public IPolygon MovePolyToTheWorld(IPolygon polygon)
        {
            double minX = polygon.EnvelopeInternal.MinX;
            double realMinX = GetRegulatedLon(minX);

            double shiftX = minX - realMinX;
            if (shiftX == 0)
            {
                return polygon;
            }

            return ShiftPolygon(polygon, shiftX);
        }

        private IPolygon MovePolyToAnotherWorld(IPolygon polygon)
        {
            double minX = polygon.EnvelopeInternal.MinX;
            double realMinX = GetRegulatedLon(minX);

            double shiftX = minX - realMinX + WorldWidth;

            return ShiftPolygon(polygon, shiftX);
        }

        private IPolygon ShiftPolygon(IPolygon polygon, double shiftX)
        {
            var points = new List<Coordinate>();
            foreach (ICoordinate p in polygon.Shell.Coordinates)
            {
                double regX = p.X - shiftX;
                var newPoint = new Coordinate(regX, p.Y);
                points.Add(newPoint);
            }

            ILinearRing ring = new LinearRing(points.ToArray());
            IPolygon shiftedPoly = _geomUtil.CreatePolygon(ring, null);

            return shiftedPoly;
        }

        private IMultiPolygon GetMultiPolygon(IGeometry geom)
        {
            var mpoly = geom as IMultiPolygon;
            if (mpoly != null)
            {
                return mpoly;
            }
            var poly = geom as IPolygon;
            if (poly != null)
            {
                return new MultiPolygon(new[] {poly});
            }
            return null;
        }

        private IMultiPolygon JoinMultiPolygon(IMultiPolygon mpoly1, IMultiPolygon mpoly2)
        {
            List<IPolygon> polyList1 = MultiPolygonToList(mpoly1);
            List<IPolygon> polyList2 = MultiPolygonToList(mpoly2);
            polyList1.AddRange(polyList2);

            return new MultiPolygon(polyList1.ToArray());
        }

        private List<IPolygon> MultiPolygonToList(IMultiPolygon mpoly)
        {
            var list = new List<IPolygon>();
            for (int i = 0; i < mpoly.Count; i++)
            {
                var poly = mpoly.GetGeometryN(i) as IPolygon;
                list.Add(poly);
            }
            return list;
        }

        #region Nested type: MinMax

        private class MinMax
        {
            public double MinX { get; set; }
            public double MaxX { get; set; }
            public double MinY { get; set; }
            public double MaxY { get; set; }
        }

        #endregion
    }
}